package com.corpgovernment.common.aspect;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.corpgovernment.common.apollo.BaseConfig;
import com.corpgovernment.common.apollo.CommonConfig;
import com.ctrip.corp.obt.generic.utils.JsonUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.client.utils.DateUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.corpgovernment.common.common.ContextUtils;
import com.corpgovernment.common.entity.Constan;
import com.corpgovernment.common.entity.LogToken;
import com.corpgovernment.common.enums.LogLevelEnum;
import com.corpgovernment.common.logging.LogUtil;
import com.ctrip.corp.obt.generic.constants.GenericConstants;
import com.ctrip.corp.obt.metric.Metrics;
import com.ctrip.corp.obt.metric.spectator.api.Id;

import lombok.extern.slf4j.Slf4j;

/**
 * @program: corpgovernment-common-logging
 * @description: 商旅政务项目aop日志切面
 * @author: wubing
 * @create: 2019-06-04 14:34
 */
@Aspect
@Component
@Slf4j
public class LogAspect {
    private static Id interfacePerformanceId = Metrics.REGISTRY.createId("common.aspect.interface.performance");

    @Value("${ignoreLogMethodNameList:test}")
    private String ignoreLogMethodNameList;

    @Value("${spring.application.name:}")
    private String serverName;

    private final static HashMap<String, String> MAP_SERVER_NAME = new HashMap<String, String>() {
        {
            put("apply-trip", "applytrip");
            put("approval-system-server", "approval");
            put("basic-manage", "basic");
            put("car-booking-server", "carBooking");
            put("flight-booking-server", "flight");
            put("flightintl-booking", "flightintl");
            put("hotel-booking-server", "hotelbooking");
            put("order-center-server", "order");
            put("organization-manage-server", "organization");
            put("pay-platform-server", "payGateway");
            put("supplier-system-server", "supplierSystem");
            put("train-booking-server", "trainBooking");
            put("bff-service", "bffService");
        }
    };

    /**
     * 根据每个服务的包名，定义controller切面日志
     * 切controller和public
     *
     * @param joinPoint
     * @return
     */
    @Around(value = "execution(* com.corpgovernment..*.controller..*.*(..)) && execution(public * *(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        return newAround(joinPoint, "controller");
    }

    /**
     * @description: 新的切面方法，为javaagent修改使用。
     * @param {ProceedingJoinPoint} joinPoint
     * @param {Integer} type controller
     * @return {*}
     */
    private Object newAround(ProceedingJoinPoint joinPoint, String type) throws Throwable {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Object[] args = joinPoint.getArgs();
        Method method = signature.getMethod();
        long start = System.currentTimeMillis();
        // 入参日志
        String input = "";
        if (args != null && args.length > 0) {
            for (Object o : args) {
                try {
                    input += JSON.toJSONString(o);
                } catch (Exception e2) {
                }
            }
        }

        Object result = null;
        try {
            result = joinPoint.proceed(args);
        } finally {
            long end = System.currentTimeMillis();

            LogToken logToken = (LogToken) ContextUtils.INSTANCE.get();
            long costTime = end - start;

            setMetric(method.getDeclaringClass().getName(), method.getName(), costTime);
            Map<String, String> mapTags = new HashMap<>();
            if (logToken != null) {
                mapTags.put("token",
                        StringUtils.isEmpty(logToken.getToken()) ? logToken.getHeaderToken() : logToken.getToken());
                mapTags.put(Constan.request_id, logToken.getRequestId());
                if (Objects.equals(type, "controller")) {
                    // 如果不走gateway网关，则需要再首次时生产requestId，以便继续串行
                    if (StringUtils.isEmpty(logToken.getRequestId())) {
                        String time = DateUtils.formatDate(new Date(), "yyyyMMddHHmmss");
                        String requestId = time.concat(UUID.randomUUID().toString().replace("-", ""));
                        log.info("当前请求requestId为null,重新初始化值，并记录当前方法名称：{}，requestId:{}", method.getName(), requestId);
                        logToken.setRequestId(requestId);
                        mapTags.put(Constan.request_id, requestId);
                    }
                }

            }

            String url = "";
            if (Objects.equals(type, "controller")) {
                RequestAttributes att;
                if ((att = RequestContextHolder.getRequestAttributes()) != null) {
                    url = ((ServletRequestAttributes)att).getRequest().getRequestURI();
                    String server = MAP_SERVER_NAME.get(serverName);
                    if (!StringUtils.isEmpty(server)) {
                        url = "/corpApi/" + server + url;
                    }
                }
                mapTags.put("【url】", url);
            }

            mapTags.put("method", method.getName());
            if (!checkLogIgnoreName(method.getName())) {
                if (BaseConfig.defaultConfig().desensitizedApis().contains(url)) {
                    // need desensitize
                    input = desensitizeJson(input);
                }
                String str =
                        String.format("【class】\n%s\n【method】\n%s\n【headerToken】\n%s\n【输入参数】\n%s\n【输出参数】\n%s\n【耗时】\n%d毫秒\n",
                                method.getDeclaringClass().getName(), method.getName(),
                                logToken == null ? null : logToken.getHeaderToken(),
                                input,
                                JSON.toJSONString(result), costTime);
                LogUtil.logging(LogLevelEnum.Info, LogAspect.class, "调用" + method.getName() + "方法", str, mapTags);
            }
        }
        return result;
    }


    private static String desensitizeJson(String json) {
        JsonNode jsonNode = null;
        try {
            jsonNode = JsonUtils.getJsonNode(json);
            // 递归遍历jsonNode，找出字段名在pathSet中的字符类型字段，对其值进行脱敏处理
            desensitizeJsonNode(jsonNode);
        } catch (Exception e) {
            log.error("Error occurs while desensitize json from Input", e);
            return json;
        }
        return Optional.ofNullable(jsonNode).map(JsonNode::toString).orElse(json);
    }

    private static void desensitizeJsonNode(JsonNode jsonNode) {
        if (jsonNode == null) {
            return;
        }
        if (jsonNode.isObject()) {
            jsonNode.fields().forEachRemaining(entry -> {
                String key = entry.getKey();
                JsonNode value = entry.getValue();
                if (value.isTextual()) {
                    // 判断key是否为需脱敏字段
                    if (!needDesensitize(key)) {
                        return;
                    }
                    // 对字段值进行脱敏处理
                    String desensitizedValue = desensitizeValue(key, value.asText());
                    if (com.ctrip.corp.obt.generic.utils.StringUtils.isNotBlank(desensitizedValue)) {
                        ((ObjectNode) jsonNode).put(key, desensitizedValue);
                    }
                } else {
                    // 递归遍历jsonNode
                    desensitizeJsonNode(value);
                }
            });
        } else if (jsonNode.isArray()) {
            jsonNode.elements().forEachRemaining(LogAspect::desensitizeJsonNode);
        }
    }


    private static boolean needDesensitize(String key) {
        return BaseConfig.defaultConfig().desensitizedKeys().contains(key);
    }

    private static String desensitizeValue(String key, String value) {
        // 根据字段名key和字段值value，对字段值进行脱敏处理
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        // 前三分之一的字符不脱敏，后三分之一的字符不脱敏，中间的字符脱敏
        int length = value.length();
        if (length == 2) {
            return value.charAt(0) + "*";
        }
        if (length == 1) {
            return "*";
        }
        int clearCount = length / 3;
        int middle = length - 2 * clearCount;
        String firstPart = value.substring(0, clearCount);
        String middlePart = String.join("", java.util.Collections.nCopies(middle, "*"));
        String lastPart = value.substring(length - clearCount);
        return firstPart + middlePart + lastPart;
    }

    /**
     * 根据每个服务的包名，定义feign service切面日志
     * 切soa
     *
     * @param joinPoint
     * @return
     */
    @Around(value = "execution(* com.corpgovernment..*.soa..*.*(..))")
    public Object aroundSoaFeign(ProceedingJoinPoint joinPoint) throws Throwable {
        return newAround(joinPoint, "soa");
    }

    /**
     * 忽略日志的方法校验  true忽略 false不忽略
     *
     * @return
     */
    private boolean checkLogIgnoreName(String methodName) {
        if (StringUtils.isEmpty(methodName) || StringUtils.isEmpty(ignoreLogMethodNameList)) {
            return false;
        }
        if (!StringUtils.isEmpty(ignoreLogMethodNameList)) {
            String[] ignoreLogMethod = ignoreLogMethodNameList.split("\\|");
            for (String s : ignoreLogMethod) {
                if (methodName.equals(s)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 获取sessionId，用来串联日志
     */
    private String getSessionId(Object[] args) {
        String sessionId = null;
        try {
            if (args != null && args.length > 0) {
                for (Object o : args) {
                    String str = o == null ? "" : JSON.toJSONString(o);
                    if (!str.startsWith("{") || !str.contains("sessionId")) {
                        continue;
                    }
                    JSONObject object = JSONObject.parseObject(str);
                    sessionId = object.getString("sessionId");
                    break;
                }
            }
            if (StringUtils.hasText(sessionId)) {
                MDC.put("SID", sessionId);
                return sessionId;
            }
            sessionId = MDC.get("SID");
            if (StringUtils.hasText(sessionId)) {
                return sessionId;
            }
        } catch (Exception e) {

        }
        sessionId = UUID.randomUUID().toString();
        return sessionId;
    }

    private void setMetric(String className, String methodName, long costTime) {
        Metrics.REGISTRY.timer(interfacePerformanceId
                .withTag("className", StringUtils.isEmpty(className) ? GenericConstants.UNKNOWN : className)
                .withTag("methodName", StringUtils.isEmpty(methodName) ? GenericConstants.UNKNOWN : methodName))
                .record(costTime, TimeUnit.MICROSECONDS);
    }
}